package com.sql.mysql.sharding.datasource;

import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.logging.Logger;

import javax.sql.DataSource;

import org.apache.ibatis.datasource.DataSourceFactory;
import org.apache.ibatis.type.JdbcType;

import com.alibaba.fastjson.JSON;
import com.sql.mysql.sharding.annotation.ShardingKeyObject;
import com.sql.mysql.sharding.config.Shard;
import com.sql.mysql.sharding.config.ShardGroup;
import com.sql.mysql.sharding.config.ShardingConfigCenter;
import com.sql.mysql.sharding.config.ShardingConfigFind;
import com.sql.mysql.sharding.threadlocal.ShardingThreadLocal;
import com.sql.mysql.sharding.utils.ShardingUrlUtils;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ShardingDataSource implements DataSource {
	// 用final保证赋值之后就不会被修改
	private final Properties propTemplate;
	private final String urlTemplate;
	// 线程可见性,只读保证线程安全性
	public static volatile List<ShardGroup> savedShardGroups = new ArrayList<ShardGroup>();

	@SuppressWarnings("unused")
	@Override
	public Connection getConnection() throws SQLException {
		// 需要根据分库分表算法来获取具体的Connection
		// 负责取到具体的MasterSlaveDataSource,然后调用其getConnection()函数
		//
		// 1)先查找后缀,如果不能转换成long就报错
		ShardingKeyObject shardingKeyObject = ShardingThreadLocal.ShardingKeyObjectThreadLocal.get();
		//ParameterMapping parameterMapping = shardingKeyObject.getParameterMapping();
		JdbcType jdbcType = shardingKeyObject.getJdbcType();
		//Class<?> javaType = parameterMapping.getJavaType();//如果是PARAMETER,则可能为空
		Object obj = shardingKeyObject.getValue();
		long shardingKey;
		if (obj instanceof Long) {
			shardingKey = (long) obj;
		} else if (obj instanceof Integer) {
			shardingKey = ((Integer) obj).intValue();
		} else {
			throw new SQLException("sharding key must be int or long");
		}
		log.debug("succeed to get sharding key {}", shardingKey);
		// 2)查找Shard
		Shard shard = ShardingConfigFind.getShard(shardingKey, savedShardGroups);
		if (null == shard) {
			log.error("fail to find shard for sharding key {}", shardingKey);
			throw new SQLException("fail to find shard for sharding key " + shardingKey);
		}
		// 3)取出MasterSlaveDataSource
		// 这个MasterSlaveDataSource会继续决定是不是要走内部读写分离
		MasterSlaveDataSource masterSlaveDataSource = (MasterSlaveDataSource) shard.getDataSource();
		if (null == masterSlaveDataSource) {
			log.error("masterSlaveDataSource is null");
			throw new SQLException("masterSlaveDataSource is null");
		}
		// 4)通过MasterSlaveDataSource拿到真正的物理连接
		return masterSlaveDataSource.getConnection();
	}

	// 远程配置中心的值+mybatis_config.xml=下面的Properties
	public ShardingDataSource(Properties prop) {
		//
		// 1)删除2个特殊变量
		String RESERVED_GLOBAL_REMOTE_CONFIG_KEY = (String) prop
				.remove(ShardingConfigCenter.RESERVED_GLOBAL_REMOTE_CONFIG_KEY);
		urlTemplate = (String) prop.remove("url");// 其实是一个格式
		log.info("succeed to delete 2 special keys");
		//
		// 2)剩下的配置模板保存起来,此模板每次创建新的DataSource时候都需要
		propTemplate = prop;
		log.info("properties {}", propTemplate);
		log.info("succeed to save properties");
		//
		// 3)解析远程拿到的配置信息
		log.info("远程配置中心 {}", RESERVED_GLOBAL_REMOTE_CONFIG_KEY);
		List<ShardGroup> currentShardGroups = (List<ShardGroup>) JSON.parseArray(RESERVED_GLOBAL_REMOTE_CONFIG_KEY,
				ShardGroup.class);
		// log.info("succeed to deserialize List<ShardGroup>
		// currentShardGroups,class {}",currentShardGroups.get(0).getClass());
		//
		// 4)按需对每个shard配置连接池,如果已经有了就复用
		injectDataSourceAndReplace(currentShardGroups);
		// 5)完毕
	}

	private void injectDataSourceAndReplace(List<ShardGroup> currentShardGroups) {
		log.info("savedShardGroups {}", savedShardGroups);
		log.info("currentShardGroups {}", currentShardGroups);
		// 遍历当前的currentShardGroups,主要目的是把连接池给引用过来
		// 下面的代码基于1个前提,连接池只增不减,所以不涉及到连接池的销毁,物理连接地址也不会发生变化
		// 这样下面的代码才是有意义的
		// 存在就引用,不存在就创建,shardName是全局唯一的
		for (ShardGroup currentShardGroup : currentShardGroups) {
			for (Shard currentShard : currentShardGroup.getShards()) {
				//
				// 每个shard,先从savedShardGroups里面查找
				for (ShardGroup savedShardGroup : savedShardGroups) {
					DataSource ds = savedShardGroup.getDataSourceByShardName(currentShard.getName());
					if (null != ds) {
						currentShard.setDataSource(ds);
						log.info("succeed to find ds for {} ,just reuse it", currentShard.getName());
						break;
					}
				}
				// 如果确实没找到,则新建1个
				if (null == currentShard.getDataSource()) {
					propTemplate.setProperty("url",
							String.format(urlTemplate, ShardingUrlUtils.union(currentShard.getMachines())));
					DataSourceFactory factory = new MasterSlaveDataSourceFactory();
					factory.setProperties(propTemplate);
					currentShard.setDataSource(factory.getDataSource());
					log.info("create one for {} because no datasource found", currentShard.getName());
				}
			}
		}
		// 果断保存, 注意线程可见性,这样读线程可以及时看到值的变化
		savedShardGroups = currentShardGroups;
		currentShardGroups = null;
	}

	@Override
	public PrintWriter getLogWriter() throws SQLException {
		throw new SQLException("not supported method called");
	}

	@Override
	public void setLogWriter(PrintWriter out) throws SQLException {
		throw new SQLException("not supported method called");
	}

	@Override
	public void setLoginTimeout(int seconds) throws SQLException {
		throw new SQLException("not supported method called");
	}

	@Override
	public int getLoginTimeout() throws SQLException {
		throw new SQLException("not supported method called");
	}

	@Override
	public Logger getParentLogger() throws SQLFeatureNotSupportedException {
		throw new SQLFeatureNotSupportedException("not supported method called");
	}

	@Override
	public <T> T unwrap(Class<T> iface) throws SQLException {
		throw new SQLException("not supported method called");
	}

	@Override
	public boolean isWrapperFor(Class<?> iface) throws SQLException {
		throw new SQLException("not supported method called");
	}

	@Override
	public Connection getConnection(String username, String password) throws SQLException {
		throw new SQLException("not supported method called");
	}

}
